
using System;
using System.Collections.Generic;
using System.Net.Http;
using UMC.Data;
using UMC.ITME;
using UMC.ITME.Entities;
using UMC.Net;

namespace UMC.ITME;

class HttpsMimeRequest : HttpMimeRequest
{

    public static StringValue authKey = new StringValue("__auth.umc");
    TlsSite tlsSite;
    HttpsMimeSocket.ClientInfo client;
    public HttpsMimeRequest(HttpsMimeSocket context, ByteChunk memory, HttpLoger loger) : base(context, memory, loger)
    {
        tlsSite = context.tlsSite1;
        this.client = context.clientInfo;
    }
    public override void StatusCode(int statusCode)
    {
        if (statusCode == 404)
        {
            this.NotCounted();
        }
    }
    protected override void Host(StringValue value)
    {
        if (tlsSite.Host.IsEmpty == false)
        {
            if (value.Span.StartsWith(tlsSite.Host.Span) == false)
            {
                this.IsHttpFormatError = true;
            }
            else
            {
                base.Host(value);
            }
        }
        else
        {
            base.Host(value);
        }
    }
    bool CheckAuthKey(out int time)
    {
        var value = authKeyValue;
        var arl = System.Buffers.ArrayPool<byte>.Shared.Rent(value.Length + tlsSite.Fingerprint.Length + 52);
        int size = Utility.DecodeBase64(value.Span, arl);
        try
        {
            var sign = arl.AsSpan(0, 16);

            time = BitConverter.ToInt32(arl.AsSpan(17, 4));

            var finger = arl.AsSpan(21, 16);
            var writer = arl.AsSpan(size);
            var ip = arl.AsSpan(37, size - 37);
            if (ip.StartsWith(_context.RemoteIpAddress.Span) == false)
            {
                if (time < Utility.TimeSpan())
                {
                    return false;
                }
            }


            if (finger.SequenceEqual(tlsSite.Finger.Span) == false)
            {

                if (this.IsWebSocket)
                {
                    if (DataFactory.Instance().Firewall(tlsSite.Finger)?.AuthType != AuthType.Allow)
                    {
                        return false;
                    }
                }
                else
                {
                    return false;
                }
            }
            WebServlet.AppId.Span.CopyTo(writer);
            WebServlet.AppSecret.Span.CopyTo(writer.Slice(16));
            var signValue = arl.AsSpan(size + 32, 16);
            Utility.MD5(arl.AsSpan(16, size + 16), signValue);

            if (signValue.SequenceEqual(sign))
            {
                if ((arl[16] & 1) == 1)
                {
                    return true;
                }
            }

        }
        finally
        {
            System.Buffers.ArrayPool<byte>.Shared.Return(arl);
        }
        return false;

    }
    StringValue authKeyValue = StringValue.Empty;


    bool isCookieValue = true;
    StringValue _cookeName = StringValue.Empty;
    bool isCookies;
    protected override int HeadValue(MimeHeadKey mimeHeadKey, ReadOnlySpan<byte> bytes)
    {
        if (mimeHeadKey == MimeHeadKey.Cookie)
        {
            isCookies = true;

            if (this.tlsSite.IsTransfer == false)
            {
                int count = 0;
                while (count < bytes.Length)
                {
                    var a = bytes[count];

                    switch (a)
                    {
                        case 13:
                            if (isCookieValue)
                            {
                                if (_cookeName.Span.SequenceEqual(authKey.Span))
                                {
                                    authKeyValue = _headChunk.Temp();
                                }
                                else
                                {
                                    _Cookies.Add(new HeadValue(_cookeName, _headChunk.Temp()));
                                }
                            }
                            else
                            {
                                isCookieValue = true;
                            }
                            this.IsValueStart = false;
                            return count + 1;
                        case (byte)'=':
                            _cookeName = _headChunk.Temp();
                            if (_cookeName.Span.SequenceEqual(authKey.Span) || WebServlet.SessionCookieName.Span.SequenceEqual(_cookeName.Span) || tlsSite.Site?.IsOutputAllCookies == true || tlsSite.Site?.OutputCookies?.TryGetValue(_cookeName, out var _, (x, y) => x.Name.Span.SequenceCompareTo(y.Span)) == true)
                            {
                                isCookieValue = true;
                            }
                            else
                            {
                                isCookieValue = false;
                                _headChunk.Scale(_cookeName.Length);
                            }

                            break;
                        case (byte)';':
                            if (isCookieValue)
                            {
                                if (_cookeName.Span.SequenceEqual(authKey.Span))
                                {
                                    authKeyValue = _headChunk.Temp();
                                }
                                else
                                {
                                    _Cookies.Add(new HeadValue(_cookeName, _headChunk.Temp()));
                                }
                            }
                            else
                            {
                                isCookieValue = true;
                            }
                            break;
                        case (byte)' ':
                            if (_headChunk.TempSize > 0)
                            {
                                _headChunk.Temp(a);
                            }
                            break;
                        default:
                            if (isCookieValue)
                                _headChunk.Temp(a);
                            break;
                    }
                    count++;
                }
                return bytes.Length;
            }
            else
            {

                return base.HeadValue(mimeHeadKey, bytes);
            }
        }
        else
        {
            return base.HeadValue(mimeHeadKey, bytes);
        }
    }
    static StringValue ChallengePath = new StringValue("/UMC.Challenge");
    protected override void Headed()
    {

        if (this.client.Error.Int32() > (tlsSite.Site?.ApiSecureLockTimes ?? 5))
        {
            _context.Challenge(_host);
            return;
        }
        netLoger.Write(HttpLogerType.Path, Method(this.Frist), this.PathQuery);
        if (_host.IsEmpty)
        {
            _host = _context.Host;
            return;
        }
        if (this.IsHttpFormatError)
        {
            return;
        }
        else if (WebServlet.AppId.IsEmpty || tlsSite.Site?.IsDisabledApiSecure == true)
        {
            _context.PrepareRespone(this);
        }
        else if (_context.IsPass == false)//.IsEmpty == false)
        {
            _context.Challenge(_host);
        }
        else if (_Challenge.IsEmpty == false)
        {
            if (ChallengePath.Span.SequenceEqual(this.Second.Span))
            {
                if (Challenge(_Challenge, out var time))
                {
                    var haskCode = new HashCode();
                    haskCode.AddBytes(_context.RemoteIpAddress.Span);
                    haskCode.AddBytes(_Challenge.Span);
                    var code = new StringValue(BitConverter.GetBytes(haskCode.ToHashCode()));
                    var auther = AuthCache.Get(new Auther { Code = code });
                    if (auther != null)
                    {

                        BitConverter.TryWriteBytes(auther.CookeTime.Span, time);
                        BitConverter.TryWriteBytes(auther.Time.Span, Utility.TimeSpan() - 120);

                    }
                    else
                    {
                        var pathSpan = GC.AllocateUninitializedArray<byte>(49, true);
                        pathSpan[0] = 0;

                        AuthCache.Put(new Auther
                        {
                            Code = code,
                            Path = new StringValue(pathSpan),
                            Quantity = new StringValue(BitConverter.GetBytes(0)),
                            Time = new StringValue(BitConverter.GetBytes(Utility.TimeSpan())),
                            CookeTime = new StringValue(BitConverter.GetBytes(time))
                        });
                    }
                    // HttpsMimeSocket httpsMime = (HttpsMimeSocket)this._context;
                    BitConverter.TryWriteBytes(this.client.Verify.Span, false);
                    BitConverter.TryWriteBytes(this.client.Error.Span, 0);
                    _context.Challenge(_host, _Challenge);

                }
                else
                {
                    _context.Challenge(_host);
                }
            }
            else
            {
                var now = Utility.TimeSpan() + 0L;
                var arl = System.Buffers.ArrayPool<byte>.Shared.Rent(120);
                int size = Utility.DecodeBase64(_Challenge.Span, arl);
                var time = BitConverter.ToUInt32(arl.AsSpan(size - 4)) + 0L;
                if (Math.Abs(time - now) < 300)
                {
                    switch (size)
                    {
                        case 20:
                            var md5 = arl.AsSpan(52);
                            if (this.tlsSite.IsHome)
                            {
                                WebServlet.AppId.Span.CopyTo(arl.AsSpan(20));
                                WebServlet.AppSecret.Span.CopyTo(arl.AsSpan(36));
                            }
                            else
                            {
                                this.tlsSite.Site.AppId.Span.CopyTo(arl.AsSpan(20));
                                this.tlsSite.Site.AppSecret.Span.CopyTo(arl.AsSpan(36));
                            }
                            Utility.MD5(arl.AsSpan(16, 36), md5);
                            if (md5.SequenceEqual(arl.AsSpan(0, 16)))
                            {
                                _context.PrepareRespone(this);
                            }
                            else
                            {
                                ApiSecure();
                            }
                            break;
                        case 68:
                            _host.Span.CopyTo(arl.AsSpan(68));
                            if (Utility.ApiVerify(arl.AsSpan(64, _host.Length + 4), arl.AsSpan(0, 64)))
                            {
                                _context.PrepareRespone(this);
                            }
                            else
                            {
                                ApiSecure();
                            }

                            break;
                        default:
                            ApiSecure();
                            break;//IsKeyStart
                    }
                }
                else
                {
                    ApiSecure();
                }
            }
        }
        else
        {
            ApiSecure();
        }

    }
    

    bool Challenge(StringValue value, out int time)
    {

        var arl = System.Buffers.ArrayPool<byte>.Shared.Rent(value.Length + tlsSite.Fingerprint.Length + 52);
        int size = Utility.DecodeBase64(value.Span, arl);
        try
        {
            var bys = arl.AsSpan(0, size);
            var sign = bys.Slice(0, 16);

            time = BitConverter.ToInt32(bys.Slice(17, 4));
            if (time < Utility.TimeSpan())
            {
                netLoger.Warn().Write("Challenge已过期").End();
                return false;
            }
            var finger = bys.Slice(21, 16);
            var writer = arl.AsSpan(size);
            var ip = bys.Slice(37, size - 37);
            if (ip.SequenceEqual(_context.RemoteIpAddress.Span) == false)
            {
                netLoger.Warn().Write("IP代理攻击").End();
                return false;
            }

            if (finger.SequenceEqual(tlsSite.Finger.Span) == false)
            {
                netLoger.Warn().Write("模拟终端攻击").End();
                return false;
            }


            WebServlet.AppId.Span.CopyTo(writer);
            WebServlet.AppSecret.Span.CopyTo(writer.Slice(16));
            var signValue = arl.AsSpan(size + 32, 16);
            using (var md5 = System.Security.Cryptography.MD5.Create())
            {
                md5.TryComputeHash(arl.AsSpan(16, size + 16), signValue, out var _);
            }
            if (signValue.SequenceEqual(sign))
            {
                var fire = DataFactory.Instance().Firewall(tlsSite.Finger);
                if (fire?.AuthType == AuthType.Deny)
                {
                    return false;
                }
                if ((arl[16] & 1) > 0)
                {
                    if ((arl[16] & 4) > 0)
                    {
                        HotCache.Put(new Firewall
                        {
                            Fingerprint = tlsSite.Finger,
                            AuthType = AuthType.Allow,
                            Expire = Utility.TimeSpan(DateTime.Now.AddDays(30)),
                            Type = 2
                        });
                    }
                    if ((arl[16] & 8) > 0)
                    {
                        HotCache.Put(new Firewall
                        {
                            Fingerprint = _context.RemoteIpAddress,
                            AuthType = AuthType.Allow,
                            Expire = Utility.TimeSpan(DateTime.Now.AddDays(30)),
                            Type = 0
                        });
                    }
                    return true;
                }
                if ((arl[16] & 2) > 0)
                {
                    if ((arl[16] & 4) > 0)
                    {
                        netLoger.Warn().Write("非可信IP").End();
                    }
                    if ((arl[16] & 8) > 0)
                    {
                        netLoger.Warn().Write("非可信终端").End();
                    }
                }

            }
            else
            {
                netLoger.Warn().Write("签名不可信").End();
            }


        }
        finally
        {
            System.Buffers.ArrayPool<byte>.Shared.Return(arl);
        }
        return false;
    }
    public override void NotCounted()
    {
        if (auther != null)
        {
            var Path = auther.Path;
            var paths = auther.Path.Span;
            int len = paths[0];
            var bvs = GC.AllocateUninitializedArray<StringValue>(len);
            int start = 1;
            for (var c = 0; c < len; c++)
            {
                bvs[c] = Path.Slice(start + c * 4 + 1, 3);
            }
            int index = Utility.Search(bvs, this.PathCode.Slice(1, 3), comparer);
            if (index >= 0)
            {
                var bytes = auther.Quantity.Span;
                for (var c = 0; c < bytes.Length; c++)
                {
                    if (bytes[c] == 0)
                    {
                        bytes[c] = 255;
                    }
                    else
                    {
                        bytes[c]--;
                        break;
                    }

                }
                paths[index * 4 + 1]--;
            }
            auther = null;
        }
    }
    Auther auther;
    StringValue PathCode;
    void ApiSecure()
    {
        var HighFrequency = tlsSite.Site?.HighFrequency ?? WebServlet.HighFrequency;

        var span = HighFrequency.Span;
        var path = this.Path.Span;
        foreach (var d in span)
        {
            if (path.StartsWith(d.StartPath.Span) && path.EndsWith(d.EndPath.Span))
            {
                _context.PrepareRespone(this);
                return;
            }
        }

        var haskCode = new HashCode();
        haskCode.AddBytes(_context.RemoteIpAddress.Span);
        int cooktime = 0;
        if (this.authKeyValue.IsEmpty)
        {

            if (_context.IsPass.HasValue == false)
            {

                if (this.isCookies || BitConverter.ToBoolean(this.client.Verify.Span) == false || this.Headers.TryGet(StringValue.SecFetchMode.Span, out var mode) == false || mode.Value.Span.SequenceEqual("cors"u8) == false)
                {
                    _context.Challenge(_host);
                    return;
                }

            }
            haskCode.AddBytes(tlsSite.Fingerprint.Span);
        }
        else if (this.CheckAuthKey(out cooktime) == false)
        {
            _context.Challenge(_host, 412, true);
            return;
        }
        else
        {
            if (tlsSite.Site != null)
            {

                if (tlsSite.Site.ApiSecureBodySize > 0 && this.ContentLength > tlsSite.Site.ApiSecureBodySize)
                {
                    _context.OutText(413, "提交内容超过限定大小");
                    return;
                }
                tlsSite.StaticPage = HttpProxy.CheckStaticPage(tlsSite.Site, this.Path.Span);
            }
            else
            {
                tlsSite.StaticPage = HttpProxy.CheckStaticPage(this.Path.Span);
            }
            if (this.tlsSite.StaticPage.Value == 0)
            {
                _context.PrepareRespone(this);
                return;
            }
            else
            {
                haskCode.AddBytes(authKeyValue.Span);
            }
        }
        tlsSite.ClientCode = haskCode.ToHashCode();


        var code = new StringValue(BitConverter.GetBytes(tlsSite.ClientCode));
        this.auther = AuthCache.Get(new Auther { Code = code });

        var hcode = new HashCode();
        hcode.AddBytes(this.Path.Span);
        var pcode = hcode.ToHashCode();
        pcode = pcode << 8;
        pcode++;
        if (auther != null)
        {
            this.PathCode = new StringValue(BitConverter.GetBytes(pcode));

            BitConverter.TryWriteBytes(auther.Quantity.Span, BitConverter.ToInt32(auther.Quantity.Span) + 1);
            bool IsHave = PathAuth(auther.Path, this.PathCode);
            var pathSpan = auther.Path.Span;

            var time = BitConverter.ToUInt32(auther.Time.Span);
            var now = Utility.TimeSpan();
            if (time + (tlsSite.Site?.ApiSecureTime ?? 60) < now)
            {
                BitConverter.TryWriteBytes(auther.Quantity.Span, 1);
                BitConverter.TryWriteBytes(auther.Time.Span, now);
                pathSpan[0] = 1;
                BitConverter.TryWriteBytes(pathSpan.Slice(1), pcode);
            }
            else
            {
                int l = pathSpan[0];
                if (pathSpan.Length >= l * 4 + 1)
                {
                    int qty = 0;
                    for (var i = 0; i < l; i++)
                    {
                        qty += pathSpan[i * 4 + 1];
                    }
                    if (BitConverter.ToInt32(auther.Quantity.Span) == qty)
                    {
                        if ((qty / l) > (tlsSite.Site?.ApiSecureRepetitions ?? 30))
                        {
                            BitConverter.TryWriteBytes(auther.Time.Span, now);
                            if (IsHave)
                            {
                                BitConverter.TryWriteBytes(auther.CookeTime.Span, now);
                                // BitConverter.TryWriteBytes(client.Error.Span, BitConverter.ToInt32(this.client.Error.Span) + 1);
                                netLoger.Warn().Write("HTTP get/post flood攻击").End();
                                _context.Challenge(_host, 429, authKeyValue.IsEmpty == false, "1.1");
                                return;
                            }
                        }
                    }
                }
            }
            if (cooktime != BitConverter.ToInt32(auther.CookeTime.Span))
            {
                _context.Challenge(_host);
                return;
            }

        }
        else
        {

            var pathSpan = GC.AllocateUninitializedArray<byte>(49, true);
            pathSpan[0] = 1;
            BitConverter.TryWriteBytes(pathSpan.AsSpan(1), pcode);

            AuthCache.Put(new Auther
            {
                Code = code,
                Path = new StringValue(pathSpan),
                Quantity = new StringValue(BitConverter.GetBytes(1)),
                Time = new StringValue(BitConverter.GetBytes(Utility.TimeSpan())),
                CookeTime = new StringValue(BitConverter.GetBytes(cooktime))
            });
        }
        BitConverter.TryWriteBytes(this.client.Verify.Span, true);
        _context.IsPass = true;
        _context.PrepareRespone(this);

    }

    static Comparer<StringValue> comparer = Comparer<StringValue>.Create((x, y) =>
    {
        return x.Span.SequenceCompareTo(y.Span);
    });
    static bool PathAuth(StringValue bytes, StringValue value)
    {
        var paths = bytes.Span;
        var maxSize = paths.Length / 4;
        int len = Math.Min(paths[0], maxSize);
        var bvs = GC.AllocateUninitializedArray<StringValue>(len);
        int start = 1;
        for (var c = 0; c < len; c++)
        {
            bvs[c] = bytes.Slice(start + c * 4 + 1, 3);

        }
        int index = Utility.Search(bvs, value.Slice(1, 3), comparer);
        if (index >= 0)
        {
            paths[index * 4 + 1]++;
            return true;
        }
        else
        {
            int vIndex = ~index;
            if (vIndex == maxSize || vIndex == maxSize - 1)
            {
                value.Span.CopyTo(paths.Slice(paths.Length - 4));
            }
            else
            {
                paths.Slice(1 + vIndex * 4, bytes.Span.Length - 5 - vIndex * 4)
                .CopyTo(paths.Slice(5 + vIndex * 4));
                value.Span.CopyTo(paths.Slice(1 + vIndex * 4));
            }
            if (len < maxSize)
            {
                paths[0]++;

            }
            return false;
        }
    }


    static UMC.Data.Caches.Cache AuthCache;
    static HttpsMimeRequest()
    {
        AuthCache = new Data.Caches.Cache("Time", 60, new Auther(), "Code");
    }
    class Auther : Record
    {
        protected override RecordColumn[] GetColumns()
        {
            var cols = new RecordColumn[5];
            cols[0] = RecordColumn.Column("Code", this.Code);
            cols[1] = RecordColumn.Column("CookeTime", this.CookeTime);
            cols[2] = RecordColumn.Column("Path", this.Path);
            cols[3] = RecordColumn.Column("Time", this.Time);
            cols[4] = RecordColumn.Column("Quantity", this.Quantity);
            return cols;
        }
        protected override void SetValue(string name, object obv)
        {

            switch (name)
            {
                case "Code":
                    this.Code = (StringValue)obv;
                    break;
                case "Path":
                    this.Path = (StringValue)obv;
                    break;
                case "Time":
                    this.Time = (StringValue)obv;
                    break;
                case "Quantity":
                    this.Quantity = (StringValue)obv;
                    break;
                case "CookeTime":
                    this.CookeTime = (StringValue)obv;
                    break;
            }
        }
        protected override void GetValues(Action<string, object> action)
        {

            AppendValue(action, "Code", this.Code);
            AppendValue(action, "Path", this.Path);
            AppendValue(action, "Time", this.Time);
            AppendValue(action, "Quantity", this.Quantity);
            AppendValue(action, "CookeTime", this.CookeTime);

        }
        public StringValue Code { get; set; } = StringValue.Empty;
        public StringValue Path { get; set; } = StringValue.Empty;
        public StringValue Time { get; set; } = StringValue.Empty;//CookeTime
        public StringValue Quantity { get; set; } = StringValue.Empty;
        public StringValue CookeTime { get; set; } = StringValue.Empty;
    }
}